package cn.seqdata.cxf.oauth2;

import java.sql.*;
import java.util.Collections;
import java.util.List;

import org.apache.cxf.rs.security.oauth2.common.AccessToken;
import org.apache.cxf.rs.security.oauth2.common.Client;
import org.apache.cxf.rs.security.oauth2.common.ServerAccessToken;
import org.apache.cxf.rs.security.oauth2.common.UserSubject;
import org.apache.cxf.rs.security.oauth2.tokens.bearer.BearerAccessToken;
import org.apache.cxf.rs.security.oauth2.tokens.refresh.RefreshToken;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.support.JdbcDaoSupport;

public class JdbcOAuthStore extends JdbcDaoSupport implements OAuthStore {
	private String tblClient = "oauth_client";
	private String keyClient = "client_id";
	private String refClientSubject = "subject";

	private String tblAccessToken = "oauth_access_token";
	private String tblRefreshToken = "oauth_refresh_token";
	private String keyToken = "token_id";
	private String valToken = "client_id,issued_at,expires_in";
	private String valAccessToken = valToken + ",refresh_token";
	private String valRefreshToken = valToken;

	private final JdbcSubjectStore subjectStore;

	public JdbcOAuthStore(JdbcSubjectStore subjectStore) {
		super();

		this.subjectStore = subjectStore;
	}

	@Override
	public List<Client> selectClients(Object subject) {
		String sql = "select " + subjectStore.getValSubject() + "," + getKeyClient() + "," + getRefClientSubject() //
			+ " from " + getTblClient() + " as c " //
			+ " join " + subjectStore.getTblSubject() + " as s on c." + getRefClientSubject() + "=s."
			+ subjectStore.getKeySubject() //
			+ " where " + getRefClientSubject() + "=?";

		return getJdbcTemplate().query(sql, clientRowMapper, subject);
	}

	@Override
	public Client selectClient(final String clientId) {
		String sql = "select " + subjectStore.getValSubject() + "," + getKeyClient() + "," + getRefClientSubject() //
			+ " from " + getTblClient() + " as c" //
			+ " join " + subjectStore.getTblSubject() + " as s on c." + getRefClientSubject() + "=s."
			+ subjectStore.getKeySubject() //
			+ " where " + getKeyClient() + "=?";

		try {
			return getJdbcTemplate().queryForObject(sql, clientRowMapper, clientId);
		} catch(EmptyResultDataAccessException ex) {
			return null;
		}
	}

	@Override
	public void deleteClient(String clientId) {
		String sql = "delete from " + getTblClient() + " where " + getKeyClient() + "=?";

		getJdbcTemplate().update(sql, clientId);
	}

	@Override
	public List<ServerAccessToken> selectClientAccessTokens(String clientId) {
		String sql = "select " + getValAccessToken() + " from " + getTblAccessToken() + " where " + getKeyClient()
			+ "=?";

		return getJdbcTemplate().query(sql, accessTokenRowMapper, clientId);
	}

	@Override
	public ServerAccessToken selectAccessToken(String tokenKey) {
		String sql = "select " + getValAccessToken() + " from " + getTblAccessToken() + " where " + getKeyToken()
			+ "=?";

		try {
			return getJdbcTemplate().queryForObject(sql, accessTokenRowMapper, tokenKey);
		} catch(EmptyResultDataAccessException ex) {
			return null;
		}
	}

	@Override
	public void updateAccessToken(ServerAccessToken token) {
		String sql = "insert into " + getTblAccessToken() + "(" + getValAccessToken() + "," + getKeyToken()
			+ ") values(?,?,?,?,?)";

		getJdbcTemplate().update(sql, ps -> {
			fromAccessToken(ps, token);
			ps.setString(4, null);
			ps.setString(5, token.getTokenKey());
		});
	}

	@Override
	public void deleteAccessToken(String tokenKey) {
		String sql = "delete from " + getTblAccessToken() + " where " + getKeyToken() + "=?";
		getJdbcTemplate().update(sql, tokenKey);
	}

	@Override
	public List<RefreshToken> selectClientRefreshTokens(String clientId) {
		String sql0 = "select " + getKeyToken() + " from " + getTblAccessToken() + " where " + getKeyClient() + "=?";
		String sql = "select " + getValRefreshToken() + " from " + getTblRefreshToken() + " where " + getKeyToken()
			+ " in(" + sql0 + ")";

		return getJdbcTemplate().query(sql, refreshTokenRowMapper, clientId);
	}

	@Override
	public RefreshToken selectRefreshToken(String tokenKey) {
		String sql = "select " + getValRefreshToken() + " from " + getTblRefreshToken() + " where " + getKeyToken()
			+ "=?";

		try {
			return getJdbcTemplate().queryForObject(sql, refreshTokenRowMapper, tokenKey);
		} catch(EmptyResultDataAccessException ex) {
			return null;
		}
	}

	@Override
	public void updateRefreshToken(RefreshToken token) {
		// insert into oauth_refresh_token(client_id,issued_at,expires_in,token_id) values(?,?,?,?)
		String sql = "insert into " + getTblRefreshToken() + "(" + getValRefreshToken() + "," + getKeyToken()
			+ ") values(?,?,?,?)";

		getJdbcTemplate().update(sql, ps -> {
			fromAccessToken(ps, token);
			ps.setString(4, token.getTokenKey());
		});
	}

	@Override
	public void deleteRefreshToken(String tokenKey) {
		String sql = "delete from " + getTblRefreshToken() + " where " + getKeyToken() + "=?";
		getJdbcTemplate().update(sql, tokenKey);
	}

	public String getTblClient() {
		return tblClient;
	}

	public void setTblClient(String tblClient) {
		this.tblClient = tblClient;
	}

	public String getKeyClient() {
		return keyClient;
	}

	public void setKeyClient(String keyClient) {
		this.keyClient = keyClient;
	}

	public String getRefClientSubject() {
		return refClientSubject;
	}

	public void setRefClientSubject(String refClientSubject) {
		this.refClientSubject = refClientSubject;
	}

	public String getTblAccessToken() {
		return tblAccessToken;
	}

	public void setTblAccessToken(String tblAccessToken) {
		this.tblAccessToken = tblAccessToken;
	}

	public String getTblRefreshToken() {
		return tblRefreshToken;
	}

	public void setTblRefreshToken(String tblRefreshToken) {
		this.tblRefreshToken = tblRefreshToken;
	}

	public String getKeyToken() {
		return keyToken;
	}

	public void setKeyToken(String keyToken) {
		this.keyToken = keyToken;
	}

	public String getValToken() {
		return valToken;
	}

	public void setValToken(String valToken) {
		this.valToken = valToken;
	}

	public String getValAccessToken() {
		return valAccessToken;
	}

	public void setValAccessToken(String valAccessToken) {
		this.valAccessToken = valAccessToken;
	}

	public String getValRefreshToken() {
		return valRefreshToken;
	}

	public void setValRefreshToken(String valRefreshToken) {
		this.valRefreshToken = valRefreshToken;
	}

	private static void fromResultSet(AccessToken token, final ResultSet rs) throws SQLException {
		Timestamp issuedAt = rs.getTimestamp(2);
		if(null != issuedAt)
			token.setIssuedAt(issuedAt.getTime() / 1000L);
		token.setExpiresIn(rs.getLong(3));
	}

	private static void fromAccessToken(PreparedStatement ps, final ServerAccessToken token) throws SQLException {
		ps.setString(1, token.getClient()
			.getClientId());
		if(token.getIssuedAt() > 0L)
			ps.setTimestamp(2, new Timestamp(token.getIssuedAt() * 1000L));
		else
			ps.setNull(2, Types.TIMESTAMP);
		ps.setLong(3, token.getExpiresIn());
	}

	private final RowMapper<Client> clientRowMapper = new RowMapper<Client>() {
		@Override
		public Client mapRow(ResultSet rs, int rowNum) throws SQLException {
			String password = rs.getString(subjectStore.getValSubject());
			String clientId = rs.getString(getKeyClient());
			String subjectId = rs.getString(getRefClientSubject());

			Client client = new Client(clientId, password, true);
			UserSubject subject = new UserSubject(clientId, subjectId);
			subjectStore.processSubject(subject);
			client.setSubject(subject);
			client.setResourceOwnerSubject(subject);

			return client;
		}
	};

	private final RowMapper<ServerAccessToken> accessTokenRowMapper = (rs, rowNum) -> {
		ServerAccessToken token = new BearerAccessToken();

		fromResultSet(token, rs);
		String clientId = rs.getString(1);
		token.setClient(selectClient(clientId));

		return token;
	};

	private final RowMapper<RefreshToken> refreshTokenRowMapper = (rs, rowNum) -> {
		RefreshToken token = new RefreshToken();

		fromResultSet(token, rs);
		token.setAccessTokens(Collections.singletonList(rs.getString(4)));

		return token;
	};
}
